﻿using System;
using System.Linq;
using System.Collections.Generic;
using Common;
using ImageProcessing.Common;

namespace ImageProcessing.Matchers
{
    /// <summary>
    /// does not scale, ugly big lock.... hmm, can i check this code in withsome elses name :-)
    /// </summary>
    public sealed class HandlerNearestNeighbour : IMatcher
    {
        private readonly List<InterestPoint> _trainedFeatures = new List<InterestPoint>();
        private const double ClosestSecondClosestRatio = 0.8d;

        private readonly object _trainMatchingLock = new object();

        #region private Process Methods

        public List<InterestPoint> Train(IEnumerable<InterestPoint> newFeatures)
        {
            var uniqueFeatures = new List<InterestPoint>();

            lock (_trainMatchingLock)
            {
                foreach (var newFeature in newFeatures)
                {
                    CheckDescriptorValidity(newFeature);

                    if (newFeature.PointInfo == null || newFeature.PointInfo.TagInfo == null)
                        throw new Exception("feature doesnt have a name!");

                    double closestDelta = double.MaxValue;
                    double secondClosestDelta = double.MaxValue;

                    var closestPoint = GetClosestStoredPoints(newFeature.Descriptor, _trainedFeatures.ToList(), newFeature.Sign,
                                                              ref secondClosestDelta, ref closestDelta);

                    if (closestPoint == null || !IsClosest(closestDelta, secondClosestDelta) &&
                        (!(Math.Abs(closestDelta - 0) < double.Epsilon ||
                           Math.Abs(secondClosestDelta - 0) < double.Epsilon)))
                    {
                        uniqueFeatures.Add(newFeature);
                        _trainedFeatures.Add(newFeature);
                    }
                }
            }

            return uniqueFeatures;
        }

        public Dictionary<TagInfo, List<FeaturesFound>> Classify(IEnumerable<InterestPoint> compareFeatures)
        {
            var objFeatureMatches = new Dictionary<TagInfo, List<FeaturesFound>>();

            // todo make multithreaded and scalable

            lock (_trainMatchingLock)
            {

                List<InterestPoint> trainedFeatures = _trainedFeatures.ToList();
                List<InterestPoint> compareBadFeatures = new List<InterestPoint>();

                foreach (var compareFeature in compareFeatures)
                {
                    double closestDelta = double.MaxValue;
                    double secondClosestDelta = double.MaxValue;

                    CheckDescriptorValidity(compareFeature);

                    var closestPoint = GetClosestStoredPoints(compareFeature.Descriptor, trainedFeatures, compareFeature.Sign,
                                                              ref secondClosestDelta, ref closestDelta);

                    if (closestPoint != null && IsClosest(secondClosestDelta, closestDelta))
                    {
                        TagInfo closestTag = closestPoint.TagInfo;

                        compareFeature.SetTagInfo(closestTag);
                        compareFeature.Matched = true;

                        if (objFeatureMatches.ContainsKey(closestTag))
                        {
                            if (compareBadFeatures.Contains(closestPoint))
                            {
                                RemoveFeatureInfo(compareFeature);
                                continue;
                            }

                            var foundClassMatched = objFeatureMatches[closestTag];

                            var alreadyInList = foundClassMatched.FirstOrDefault(x => x.KnownInterestPoints == closestPoint);
                            if (alreadyInList != null)
                            {
                                foundClassMatched.Remove(alreadyInList);
                                compareBadFeatures.Add(closestPoint);

                                RemoveFeatureInfo(alreadyInList.ComparisonPoints);
                                RemoveFeatureInfo(compareFeature);
                                continue;
                            }

                            var ff = new FeaturesFound(compareFeature, closestPoint);
                            foundClassMatched.Add(ff);
                        }
                        else
                        {
                            objFeatureMatches.Add(closestTag, new List<FeaturesFound>());
                            objFeatureMatches[closestTag].Add(new FeaturesFound(compareFeature, closestPoint));
                        }
                    }
                }

                foreach (var objectClass in objFeatureMatches.ToList())
                {
                    if (objectClass.Value.Count == 0)
                        objFeatureMatches.Remove(objectClass.Key);
                }
            }

            return objFeatureMatches;
        }

        private static void RemoveFeatureInfo(InterestPoint compareFeature)
        {
            compareFeature.SetTagInfo(null);
            compareFeature.Matched = false;
        }

        private static void CheckDescriptorValidity(InterestPoint compareFeature)
        {
            if (compareFeature == null)
                throw new ArgumentNullException("compareFeature");

            if (!compareFeature.IsValidDescriptor)
                throw new Exception("Descriptor not valid");
        }

        private static bool IsClosest(double secondClosestDelta, double closestDelta)
        {
            return closestDelta / secondClosestDelta < ClosestSecondClosestRatio;
        }

        private InterestPoint GetClosestStoredPoints(float[] compareDescriptor, List<InterestPoint> trainedFeatures, int sign, ref double secondClosestDelta, ref double closestDelta)
        {
            InterestPoint aClosest = null;
            foreach (var storedFeature in trainedFeatures)
            {
                int aSignStore = storedFeature.Sign;
                if (sign != aSignStore)
                {
                    continue;
                }

                float[] aDescriptorStore = storedFeature.Descriptor;

                double desciptorsSummedDifference = 0;

                for (int a = 0; a < compareDescriptor.Length; ++a)
                {
                    desciptorsSummedDifference += Math.Abs(compareDescriptor[a] - aDescriptorStore[a]);

                    if (desciptorsSummedDifference >= secondClosestDelta)
                    {
                        break;
                    }
                }

                if (desciptorsSummedDifference < closestDelta)
                {
                    secondClosestDelta = closestDelta;
                    closestDelta = desciptorsSummedDifference;
                    aClosest = storedFeature;

                    if (desciptorsSummedDifference < double.Epsilon)
                        break;
                }
                else if (Math.Abs(desciptorsSummedDifference - closestDelta) < double.Epsilon)
                {
                    secondClosestDelta = closestDelta;
                }
                else if (desciptorsSummedDifference < secondClosestDelta)
                {
                    secondClosestDelta = desciptorsSummedDifference;
                }
            }
            return aClosest;
        }

        #endregion

        public void ImportPoints(List<InterestPoint> points)
        {
            _trainedFeatures.AddRange(points);
        }

        public List<InterestPoint> ExportPoints()
        {
            return _trainedFeatures.ToList();
        }

        public void Clear()
        {
            _trainedFeatures.Clear();
        }
    }
}
